#include <iostream>
#include <thread>
#include <vector>
#include <fstream>
#include <boost/optional.hpp>
#include <boost/array.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/lexical_cast.hpp>

#include "BackupFilesUploader.h"
#include "FileSegmentHeader.h"

/** Temps (en secondes) d'attente entre chaque verification du dossier d'envoi. */
#define DIR_CHECK_TIMEOUT 10
/** Taille du buffer d'envoi de fichier. */
#define UPLOAD_BUFFER_SIZE 512
/** Taille d'un segment de fichier. */
#define FILE_SEGMENT_SIZE 400
/** Nombre de sauvegardes a realiser pour chaque fiche par defaut. */
#define DEFAULT_NUMBER_OF_BACKUPS 3
/** Temps d'attente apres un envoi de segment echoue. */
#define SENDING_FAILED_TIMEOUT 60*5


namespace sys = boost::system;
namespace fs = boost::filesystem;
namespace ip = boost::asio::ip;


namespace
{

typedef std::vector<ip::udp::endpoint> UdpIpArray;


/**
 * Hasher un ID de patient.
 *
 * @param patientId Identifiant a hasher.
 * @return Resultat du hashage.
 */
uint32_t hash_patient_id(const uint32_t& patientId)
{
	uint32_t res = patientId;

	res = (res+0x7ed55d16) + (res<<12);
    res = (res^0xc761c23c) ^ (res>>19);
    res = (res+0x165667b1) + (res<<5);
    res = (res+0xd3a2646c) ^ (res<<9);
    res = (res+0xfd7046c5) + (res<<3);
    res = (res^0xb55a4f09) ^ (res>>16);

	return res;
}


/**
 * Choisir le serveur de sauvegarde pour un fichier, en fonction de son nom et du
 * nombre de copies deja effectuees.
 *
 * @param fileName Nom du fichier.
 * @param copyNum Nombre de copies deja effectuees.
 */
const ip::udp::endpoint& choose_backup_server(const UdpIpArray& serverIps,
	const std::string& fileName, const uint32_t& copyNum)
{
	static const std::vector<std::string> RECORD_ACRONYMS({"FEC", "FCT", "FMT", "FMN"});

	if (serverIps.empty())
		throw std::invalid_argument("server list empty");

	size_t computerIdEndPos = fileName.find_first_of('_');
	size_t patientIdEndPos;
	uint32_t patientId;
	std::string recordAcronym;
	size_t acronymNum = 0;

	if (computerIdEndPos == std::string::npos)
		throw std::invalid_argument("file name does not contain a computer ID");

	patientIdEndPos = fileName.find_first_of('_', computerIdEndPos+1);

	if (patientIdEndPos == std::string::npos)
		throw std::invalid_argument("file name does not contain a patient ID");

	/* Extraire l'acronyme de la fiche et l'ID du patient du nom du fichier. */
	patientId = boost::lexical_cast<uint32_t>(fileName.substr(computerIdEndPos+1,
			patientIdEndPos-computerIdEndPos-1));
	recordAcronym = fileName.substr(patientIdEndPos+1);

	/* Chercher l'acronyme de fiche extrait parmi les acronymes connus. */
	while (acronymNum < RECORD_ACRONYMS.size()
		&& RECORD_ACRONYMS[acronymNum] != recordAcronym)
	{
		++acronymNum;
	}

	if (acronymNum == RECORD_ACRONYMS.size())
		throw std::invalid_argument("unknown record type");

	/* Initialiser le generateur de nombres aleatoires avec l'ID du patient, auquel est
	 * ajoute le numero de la copie. */
	std::default_random_engine rng(hash_patient_id(patientId)+copyNum);

	/* Passer les nombres utilises par les fiches dont l'acronyme precede celui de la
	 * fiche courante. */
	rng.discard(acronymNum);

	/* Retourner l'IP du serveur a l'index genere aleatoirement. */
	return serverIps[static_cast<float>(rng())*(serverIps.size()-1)/rng.max()];
}

} // end of anonymous namespace


BackupFilesUploader::BackupFilesUploader(const LogWriter& logWriter) :
	m_logWriter(logWriter)
{}


void BackupFilesUploader::set_monitored_directory(
	const std::string& monitoredDirPathStr)
{
	m_monitoredDirPath = monitoredDirPathStr;
}


void BackupFilesUploader::set_destination_servers(const UdpIpList& destServerIps)
{
	m_destServerIps = destServerIps;
}


void BackupFilesUploader::run()
try
{
	while (true)
	{
		try
		{
			if (!fs::exists(m_monitoredDirPath) || !fs::is_directory(m_monitoredDirPath))
				m_logWriter.write_warning("No directory to monitor.");
			else
			{
				for (fs::directory_iterator current(m_monitoredDirPath), last;
					 current != last;
					 ++current)
				{
					try
					{
						/* Ignorer les elements n'etant pas des fichiers reguliers. */
						if (fs::is_symlink(*current) || !fs::is_regular_file(*current))
							continue;

						/* Ecrire un petit message de log. */
						{
							std::stringstream ss;
							ss << "Backing up file " << current->path() << ".";
							m_logWriter.write_info(ss.str());
						}

						backup_file(current->path());

						fs::remove(current->path());
					}
					catch (std::exception& exc)
					{
						m_logWriter.write_error(exc.what());
					}
				}
			}
		}
		catch (std::exception& exc)
		{
			m_logWriter.write_error(exc.what());
		}

		/* Attendre avant de verifier le dossier a nouveau. */
		std::this_thread::sleep_for(std::chrono::seconds(DIR_CHECK_TIMEOUT));
	}
}
catch (std::exception& exc)
{
	m_logWriter.write_error(exc.what());
}


void BackupFilesUploader::upload_file_to_one(const fs::path& filePath,
	const ip::udp::endpoint& serverEndpoint)
{
	ip::udp::socket socket(m_ioService);
	std::ifstream uploadFile(filePath.string());
	std::array<char, UPLOAD_BUFFER_SIZE> buffer;
	size_t readSize = 0;
	FileSegmentHeader filePartHeader(filePath.filename().string());
	const size_t partHeaderSize = filePartHeader.size();
	boost::system::error_code resCode;

	if (!uploadFile.good())
	{
		std::stringstream ss;
		ss << "failed to open input file \"" << filePath.native() << "\"";
		throw std::invalid_argument(ss.str());
	}

	socket.open(ip::udp::v4());

	/* Envoyer tout le fichier en petits morceaux. */
	for (size_t segmentNum=0;
		 (readSize = uploadFile.readsome(buffer.data()+partHeaderSize,
			 FILE_SEGMENT_SIZE)) > 0;
		 ++segmentNum)
	{
		bool fragmentSent = false;

		/* Mettre a jour les informations de l'en-tete, et l'ecrire dans le buffer. */
		filePartHeader.segmentNum = segmentNum;
		filePartHeader.write_to(buffer.data(), buffer.size());

		/* Envoyer le segment courant, en reessayant jusqu'a ce qu'a avoir reussi. */
		while (!fragmentSent)
		{
			socket.send_to(boost::asio::buffer(buffer, partHeaderSize+readSize),
				serverEndpoint, 0, resCode);

			if (resCode == boost::system::errc::success)
				fragmentSent = true;
			/* Attendre un certain temps avant de retenter un envoi. */
			else
				std::this_thread::sleep_for(std::chrono::seconds(SENDING_FAILED_TIMEOUT));
		}
	}

	socket.close();
}


void BackupFilesUploader::backup_file(const fs::path& filePath)
{
	const UdpIpArray serverIps(m_destServerIps.begin(), m_destServerIps.end());
	const std::string fileName(filePath.filename().string());
	const uint32_t nbOfCopies = std::min<uint32_t>(m_destServerIps.size(),
		DEFAULT_NUMBER_OF_BACKUPS);	

	/* Envoie le nombre requis de copies. */
	for (uint32_t i=0; i < nbOfCopies; ++i)
		upload_file_to_one(filePath, choose_backup_server(serverIps, fileName, i));
}
